package com.idanchuang.component.just.web.filter;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.idanchuang.component.base.JsonResult;
import com.idanchuang.component.core.Dc;
import com.idanchuang.component.core.ServiceInfo;
import com.idanchuang.component.core.helper.EnvContext;
import com.idanchuang.component.just.web.config.ComponentConfig;
import com.idanchuang.component.just.web.log.AccessLogger;
import com.idanchuang.component.just.web.util.IpUtils;
import com.idanchuang.component.shadow.core.ShadowContextMonitor;
import org.apache.skywalking.apm.toolkit.trace.TraceContext;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.MethodParameter;
import org.springframework.core.NamedThreadLocal;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;
import org.springframework.web.util.ContentCachingRequestWrapper;
import org.springframework.web.util.WebUtils;

import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.UnsupportedEncodingException;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.concurrent.atomic.AtomicLong;

/**
 * 访问日志
 */
@RestControllerAdvice
public class WebAccessFilter implements InnerWebFilter, ResponseBodyAdvice {
    private static final Logger logger = LoggerFactory.getLogger(WebAccessFilter.class);
    /**
     * 处理时间 线程绑定
     */
    private static final ThreadLocal<Long> DEAL_TIME_THREAD_LOCAL = new NamedThreadLocal<>("WebAccessDealTime");
    private static final ThreadLocal<JsonResult> RESULT_THREAD_LOCAL = new ThreadLocal<>();

    /**
     * resultMsg 长度限制
     */
    private static final int MAX_RESULT_MSG_LENGTH = 50;

    private static volatile AtomicLong SEQ = new AtomicLong(0);

    private final ComponentConfig componentConfig;

    public WebAccessFilter(ComponentConfig componentConfig) {
        this.componentConfig = componentConfig;
    }

    @Override
    public boolean matchUrlPatterns(ServletRequest req) {
        return true;
    }

    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        return isEnabled();
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                  Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        if (body instanceof JsonResult) {
            RESULT_THREAD_LOCAL.set((JsonResult) body);
        }
        return body;
    }

    @Override
    public boolean preFilter(ServletRequest req, ServletResponse res) {
        // if enabled
        if (isEnabled()) {
            DEAL_TIME_THREAD_LOCAL.set(System.currentTimeMillis());
        }
        return true;
    }

    @Override
    public void postFilter(ServletRequest req, ServletResponse res) {
        try {
            // if disabled
            if (!isEnabled()) {
                return;
            }
            Long start = DEAL_TIME_THREAD_LOCAL.get();
            if (start == null) {
                return;
            }
            long end = System.currentTimeMillis();
            // 访问日志
            doAccessLog(req, res, start, end);
        } finally {
            DEAL_TIME_THREAD_LOCAL.remove();
            RESULT_THREAD_LOCAL.remove();
        }
    }

    /**
     * 打印访问日志
     *
     * @param req   请求
     * @param res   响应
     * @param start 请求起始时间
     * @param end   请求结束时间
     */
    private void doAccessLog(ServletRequest req, ServletResponse res, long start, long end) {
        try {
            long rt = end - start;
            HttpServletRequest request = (HttpServletRequest) req;
            HttpServletResponse response = (HttpServletResponse) res;
            // resolve url for pattern
            String url = resolveUrl(request);
            if (skipUrl(url, response)) {
                return;
            }

            // 是否采样
            long seq = SEQ.incrementAndGet();
            boolean isSampling = isSampling(seq, componentConfig.getLogResultProbability());

            Access access = new Access();
            access.setUrl(url);
            access.setAppId(ServiceInfo.getAppId().toLowerCase());
            access.setGroup(ServiceInfo.getGroup());
            access.setVersion(ServiceInfo.getVersion());
            access.setTimestamp(start);
            access.setTime(formatTime(start));
            access.setProtocol(request.getProtocol());
            access.setMethod(request.getScheme());
            access.setRequestMethod(request.getMethod());
            access.setHost(req.getServerName());
            access.setPort(request.getServerPort());
            access.setQuery(formatParams(request));
            access.setServerAddr(ServiceInfo.address());
            access.setTraceId(EnvContext.getDcTraceId());
            String skyId = TraceContext.traceId();
            if (!"".equals(skyId)) {
                access.setSkyId(skyId);
            }
            access.setApmId(org.slf4j.MDC.get("apm-traceid"));
            access.setRequestTime(rt);
            access.setUserId(request.getHeader("userId"));
            access.setStatus(response.getStatus());
            access.setClientIp(request.getRemoteHost());
            access.setRealClientIp(IpUtils.getClientIp(request));
            access.setRpcFrom(getRpcFromAppId(request).toLowerCase());
            access.setFromApi(getRpcFromApi(request));
            access.setFromGroup(getRpcFromGroup(request));
            access.setFromVersion(getRpcFromVersion(request));
            access.setReferer(request.getHeader("referer"));
            access.setUserAgent(request.getHeader("user-agent"));
            access.setOriginReq(EnvContext.getAttribute(Dc.ORIGIN_REQUEST));
            JsonResult result = RESULT_THREAD_LOCAL.get();
            if (result != null) {
                access.setResultCode(String.valueOf(result.getCode()));
                access.setShortCode(result.getShortCode());
                // resultMsg 限制长度
                String resultMsg = result.getMsg();
                if (resultMsg != null && resultMsg.length() > MAX_RESULT_MSG_LENGTH) {
                    resultMsg = resultMsg.substring(0, MAX_RESULT_MSG_LENGTH);
                }
                access.setResultMsg(resultMsg);
            }
            // 打印header
            if (isLogHeaders(access.getStatus(), result, isSampling)) {
                access.setHeaders(formatHeaders(request));
            }
            //打印request
            if (isLogRequest(access.getStatus(), result, isSampling)) {
                access.setRequest(getMessagePayload(request));
            }
            // 打印result
            if (isLogResult(access.getStatus(), result, isSampling)) {
                access.setResult(result.toJsonString());
            }
            // 是否压测请求
            access.setPressure(EnvContext.isPressure() || ShadowContextMonitor.isShadowEnvironment() ? 1 : 0);
            // 打印至access
            AccessLogger.info(access);
        } catch (Exception e) {
            logger.warn("webAccessFilter doAccessLog failed", e);
        }
    }

    /**
     * 是否跳过此请求
     *
     * @param url      url
     * @param response 响应
     * @return ..
     */
    private boolean skipUrl(String url, HttpServletResponse response) {
        if (componentConfig.isIgnoreLogHealth() && url.endsWith("/actuator/health")) {
            return true;
        }
        if (componentConfig.isIgnoreLogResource()) {
            String contentType = response.getContentType();
            return contentType.startsWith("application/javascript")
                    || contentType.startsWith("text/css")
                    || contentType.startsWith("image/")
                    || contentType.startsWith("video/")
                    || contentType.startsWith("text/html");
        }
        return false;
    }

    /**
     * 是否打印header
     *
     * @param result 结果
     * @return ..
     */
    private boolean isLogHeaders(int httpCode, JsonResult result, boolean isSampling) {
        if (httpCode >= 300) {
            return true;
        }
        // 采样
        if (isSampling) {
            return true;
        }
        // 开启全量录制
        if (componentConfig.isLogFull()) {
            return true;
        }
        // 开启header打印
        if (componentConfig.isLogHeaders()) {
            return true;
        }
        // 非业务请求
        if (result == null) {
            return false;
        }
        // 失败的请求
        if (!result.isSuccess()) {
            return true;
        }
        // 是否为诊断请求
        return EnvContext.isDiagnoses();
    }

    /**
     * 是否打印响应结果
     *
     * @param result 结果
     * @return ..
     */
    private boolean isLogResult(int httpCode, JsonResult result, boolean isSampling) {
        // 非业务请求
        if (result == null) {
            return false;
        }
        // 失败的请求
        if (httpCode >= 300 || !result.isSuccess()) {
            return true;
        }
        // 采样
        if (isSampling) {
            return true;
        }
        // 开启全量录制
        if (componentConfig.isLogFull()) {
            return true;
        }
        // 开启 result 打印
        if (componentConfig.isLogResult()) {
            return true;
        }
        //比较采样率
        if (componentConfig.getLogResultProbability() > 0
                && componentConfig.getLogResultProbability() > Math.random()) {
            return true;
        }
        // 是否为诊断请求
        return EnvContext.isDiagnoses();
    }

    /**
     * 是否打印请求体
     *
     * @return boolean
     */
    private boolean isLogRequest(int httpCode, JsonResult result, boolean isSampling) {
        // 失败的请求
        if (httpCode >= 300) {
            return true;
        }
        if (result != null && !result.isSuccess()) {
            return true;
        }
        // 采样
        if (isSampling) {
            return true;
        }
        // 开启全量录制
        if (componentConfig.isLogFull()) {
            return true;
        }
        //记录请求体
        if (componentConfig.isLogRequest()) {
            return true;
        }
        // 是否为诊断请求
        return EnvContext.isDiagnoses();
    }

    /**
     * 如果匹配url模板则使用模板, 未匹配模板则使用当前url
     *
     * @param request 当前请求
     * @return ..
     */
    private static String resolveUrl(HttpServletRequest request) {
        String url = EnvContext.getCurrentPath();
        if (url == null) {
            url = request.getRequestURI();
        }
        String[] patterns = ComponentConfig.accessUrlPatternList;
        if (patterns == null || patterns.length == 0) {
            return url;
        }
        AntPathMatcher antPathMatcher = new AntPathMatcher();
        for (String pattern : patterns) {
            if (pattern == null || pattern.isEmpty()) {
                continue;
            }
            if (antPathMatcher.match(pattern, url)) {
                return pattern;
            }
        }
        return url;
    }

    /**
     * 根据采样率判断是否打印全量信息
     * @return 是否采样
     */
    private static boolean isSampling(long seq, double probability) {
        if (probability == 0) {
            return false;
        }
        int c = (int)(1 / probability);
        return (seq % c) == 0;
    }

    private static String formatTime(long time) {
        Date date = new Date(time);
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS");
        return sdf.format(date);
    }

    private static String getRpcFromAppId(HttpServletRequest request) {
        String rpcFrom = request.getHeader(Dc.RPC_FROM_NAME);
        if (rpcFrom == null) {
            rpcFrom = request.getParameter(Dc.RPC_FROM_NAME);
        }
        return rpcFrom != null ? rpcFrom : Dc.DEFAULT_RPC_FROM;
    }

    private static String getRpcFromApi(HttpServletRequest request) {
        String rpcFrom = request.getHeader(Dc.RPC_FROM_API_NAME);
        return rpcFrom != null ? rpcFrom : Dc.DEFAULT_RPC_FROM_API;
    }

    private static String getRpcFromGroup(HttpServletRequest request) {
        String rpcFrom = request.getHeader(Dc.RPC_FROM_GROUP_NAME);
        return rpcFrom != null ? rpcFrom : Dc.DEFAULT_RPC_FROM_GROUP;
    }

    private static String getRpcFromVersion(HttpServletRequest request) {
        String rpcFrom = request.getHeader(Dc.RPC_FROM_VERSION_NAME);
        return rpcFrom != null ? rpcFrom : Dc.DEFAULT_RPC_FROM_VERSION;
    }

    private boolean isEnabled() {
        return this.componentConfig != null && this.componentConfig.isAccessLog();
    }

    /**
     * 获取请求体
     *
     * @param request 请求
     * @return {@link String}
     */
    protected String getMessagePayload(HttpServletRequest request) {
        ContentCachingRequestWrapper wrapper =
                WebUtils.getNativeRequest(request, ContentCachingRequestWrapper.class);
        if (wrapper != null) {
            byte[] buf = wrapper.getContentAsByteArray();
            if (buf.length > 0) {
                try {
                    return new String(buf, wrapper.getCharacterEncoding());
                } catch (UnsupportedEncodingException ex) {
                    return "[unknown]";
                }
            }
        }
        return "";
    }

    private static String formatParams(HttpServletRequest request) {
        Map<String, String[]> params = request.getParameterMap();
        if (params == null || params.isEmpty()) {
            return null;
        }
        return JSON.toJSONString(params);
    }

    private static String formatHeaders(HttpServletRequest request) {
        Map<String, List<String>> map = new HashMap<>();
        for (String headerName : Collections.list(request.getHeaderNames())) {
            map.put(headerName, Collections.list(request.getHeaders(headerName)));
        }
        return map.toString();
    }

    private static class Access {

        /**
         * 服务
         */
        String appId;
        /**
         * 服务版本
         */
        String version;
        /**
         * 服务分组
         */
        String group;
        /**
         * 请求开始时间
         */
        String time;
        /**
         * 请求开始时间
         */
        long timestamp;
        /**
         * 请求host
         */
        String host;
        /**
         * 服务端口
         */
        int port;
        /**
         * 链路id
         */
        String traceId;
        /**
         * skywalking 的traceId
         */
        String skyId;
        /**
         * apm 的 traceId
         */
        String apmId;
        /**
         * 服务地址
         */
        String serverAddr;
        /**
         * http响应码
         */
        int status;
        /**
         * 响应延迟
         */
        long requestTime;
        /**
         * url
         */
        String url;
        /**
         * 请求类型
         */
        String method;
        /**
         * 请求参数
         */
        String query;
        /**
         * 请求方式
         */
        String requestMethod;
        /**
         * 请求协议
         */
        String protocol;
        /**
         * 调用方ip
         */
        String clientIp;
        /**
         * 真实源ip
         */
        String realClientIp;
        /**
         * 调用方
         */
        String rpcFrom;
        /**
         * 调用方接口
         */
        String fromApi;
        /**
         * 调用方分组
         */
        String fromGroup;
        /**
         * 调用方应用版本
         */
        String fromVersion;
        /**
         * 错误信息
         */
        String resultMsg;
        /**
         * 业务码
         */
        String resultCode;
        /**
         * 业务码-短码
         */
        int shortCode;
        /**
         * 是否压测请求 0否 1是
         */
        int pressure;
        /**
         * 用户id
         */
        String userId;
        /**
         * 请求头
         */
        String headers;
        /**
         * 请求体
         */
        String request;
        /**
         * 响应结果
         */
        String result;
        /**
         * 来源页面
         */
        String referer;
        /**
         * 客户端信息
         */
        String userAgent;
        /**
         * 链路入口path
         */
        String originReq;

        public long getTimestamp() {
            return timestamp;
        }

        public void setTimestamp(long timestamp) {
            this.timestamp = timestamp;
        }

        public String getResultMsg() {
            return resultMsg;
        }

        public int getPressure() {
            return pressure;
        }

        public void setPressure(int pressure) {
            this.pressure = pressure;
        }

        public void setResultMsg(String resultMsg) {
            this.resultMsg = resultMsg;
        }

        public String getUserId() {
            return userId;
        }

        public void setUserId(String userId) {
            this.userId = userId;
        }

        public String getReferer() {
            return referer;
        }

        public void setReferer(String referer) {
            this.referer = referer;
        }

        public String getUserAgent() {
            return userAgent;
        }

        public void setUserAgent(String userAgent) {
            this.userAgent = userAgent;
        }

        public String getResultCode() {
            return resultCode;
        }

        public void setResultCode(String resultCode) {
            this.resultCode = resultCode;
        }

        public int getShortCode() {
            return shortCode;
        }

        public void setShortCode(int shortCode) {
            this.shortCode = shortCode;
        }

        public String getVersion() {
            return version;
        }

        public void setVersion(String version) {
            this.version = version;
        }

        public String getResult() {
            return result;
        }

        public void setResult(String result) {
            this.result = result;
        }

        public String getApmId() {
            return apmId;
        }

        public void setApmId(String apmId) {
            this.apmId = apmId;
        }

        public String getGroup() {
            return group;
        }

        public void setGroup(String group) {
            this.group = group;
        }

        public String getFromGroup() {
            return fromGroup;
        }

        public void setFromGroup(String fromGroup) {
            this.fromGroup = fromGroup;
        }

        public String getFromVersion() {
            return fromVersion;
        }

        public String getHeaders() {
            return headers;
        }

        public void setHeaders(String headers) {
            this.headers = headers;
        }

        public void setFromVersion(String fromVersion) {
            this.fromVersion = fromVersion;
        }

        public String getAppId() {
            return appId;
        }

        public void setAppId(String appId) {
            this.appId = appId;
        }

        public String getHost() {
            return host;
        }

        public void setHost(String host) {
            this.host = host;
        }

        public int getPort() {
            return port;
        }

        public String getTraceId() {
            return traceId;
        }

        public void setTraceId(String traceId) {
            this.traceId = traceId;
        }

        public String getServerAddr() {
            return serverAddr;
        }

        public void setServerAddr(String serverAddr) {
            this.serverAddr = serverAddr;
        }

        public void setPort(int port) {
            this.port = port;
        }

        public String getTime() {
            return time;
        }

        public void setTime(String time) {
            this.time = time;
        }

        public int getStatus() {
            return status;
        }

        public void setStatus(int status) {
            this.status = status;
        }

        public long getRequestTime() {
            return requestTime;
        }

        public void setRequestTime(long requestTime) {
            this.requestTime = requestTime;
        }

        public String getUrl() {
            return url;
        }

        public String getFromApi() {
            return fromApi;
        }

        public void setFromApi(String fromApi) {
            this.fromApi = fromApi;
        }

        public void setUrl(String url) {
            this.url = url;
        }

        public String getMethod() {
            return method;
        }

        public void setMethod(String method) {
            this.method = method;
        }

        public String getQuery() {
            return query;
        }

        public void setQuery(String query) {
            this.query = query;
        }

        public String getRequestMethod() {
            return requestMethod;
        }

        public void setRequestMethod(String requestMethod) {
            this.requestMethod = requestMethod;
        }

        public String getProtocol() {
            return protocol;
        }

        public void setProtocol(String protocol) {
            this.protocol = protocol;
        }

        public String getClientIp() {
            return clientIp;
        }

        public void setClientIp(String clientIp) {
            this.clientIp = clientIp;
        }

        public String getRealClientIp() {
            return realClientIp;
        }

        public void setRealClientIp(String realClientIp) {
            this.realClientIp = realClientIp;
        }

        public String getRpcFrom() {
            return rpcFrom;
        }

        public String getSkyId() {
            return skyId;
        }

        public void setSkyId(String skyId) {
            this.skyId = skyId;
        }

        public String getOriginReq() {
            return originReq;
        }

        public void setOriginReq(String originReq) {
            this.originReq = originReq;
        }

        public void setRpcFrom(String rpcFrom) {
            this.rpcFrom = rpcFrom;
        }

        public String getRequest() {
            return request;
        }

        public void setRequest(String request) {
            this.request = request;
        }

        @Override
        public String toString() {
            return JSON.toJSONString(this, SerializerFeature.IgnoreNonFieldGetter);
        }
    }


}
